using System;
using System.Drawing;
using System.ComponentModel;
using System.Windows.Forms;
using System.Runtime.Serialization;

using SharpGL.Media3D;
using SharpGL.SceneGraph.Helpers;

namespace SharpGL.SceneGraph.Transformations
{
    /// <summary>
    /// The ArcBallTransformation camera supports arcball projection, making it ideal for use with a mouse.
    /// </summary>
    [TypeConverter(typeof(ExpandableObjectConverter))]
    [DataContract(/*IsReference = true, */Name = "ArcBallTransformation", Namespace = "SharpGL.SceneGraph")]
    public class ArcBallTransformation : Transformation
    {
        #region Private Types

        private struct Quaternion
        {
            public float x, y, z, w;

            public Quaternion(float x, float y, float z, float w)
            {
                this.x = x;
                this.y = y;
                this.z = z;
                this.w = w;
            }
        }

        #endregion Private Types

        #region Private Consts

        private const float Epsilon = 1.0e-5f;

        private static readonly Matrix defaultTransformationMatrix = new Matrix(Matrix.Identity(4));
        private static readonly Matrix defaultLastTransformationMatrix = new Matrix(Matrix.Identity(4));

        private static readonly Size defaultSize = new Size(1, 1);

        private static readonly Vector3D defaultStartVector = new Vector3D(0, 0, 0);

        private static readonly Point defaultMouseStartDragPoint = new Point(0, 0);
        private static readonly bool defaultIsLeftDrag = false;
        private static readonly bool defaultIsRightDrag = false;
        private static readonly bool defaultIsMiddleDrag = false;

        #endregion Private Consts

        #region Private Fields

        [IgnoreDataMember()]
        private Matrix transformationMatrix;
        [IgnoreDataMember()]
        private Matrix lastTransformationMatrix;

        [IgnoreDataMember()]
        private Size size;

        [IgnoreDataMember()]
        private Vector3D startVector;

        [IgnoreDataMember()]
        private Point mouseStartDragPoint;
        [IgnoreDataMember()]
        private bool isLeftDrag;
        [IgnoreDataMember()]
        private bool isRightDrag;
        [IgnoreDataMember()]
        private bool isMiddleDrag;

        #endregion Private Fields

        #region Private Methods

        private Quaternion CalculateQuaternion(Vector3D startVector, Vector3D endVector)
        {
            //  Compute the cross product of the begin and end vectors.
            Vector3D cross = Vector3D.CrossProduct(startVector, endVector);

            //  Is the perpendicular length essentially non-zero?
            if (cross.Length > Epsilon)
            {
                //  The quaternion is the transform.
                return new Quaternion(cross.X, cross.Y, cross.Z, Vector3D.DotProduct(startVector, endVector));
            }
            else
            {
                //  Begin and end coincide, return identity.
                return new Quaternion(0.0f, 0.0f, 0.0f, 0.0f);
            }
        }

        private Matrix RotationMatrixFromQuaternion(Quaternion quaternion)
        {
            float n, s;
            float xs, ys, zs;
            float wx, wy, wz;
            float xx, xy, xz;
            float yy, yz, zz;
            n = (quaternion.x * quaternion.x) + (quaternion.y * quaternion.y) + (quaternion.z * quaternion.z) + (quaternion.w * quaternion.w);
            s = (n > 0.0f) ? (2.0f / n) : 0.0f;

            xs = quaternion.x * s;
            ys = quaternion.y * s;
            zs = quaternion.z * s;
            wx = quaternion.w * xs;
            wy = quaternion.w * ys;
            wz = quaternion.w * zs;
            xx = quaternion.x * xs;
            xy = quaternion.x * ys;
            xz = quaternion.x * zs;
            yy = quaternion.y * ys;
            yz = quaternion.y * zs;
            zz = quaternion.z * zs;

            Matrix matrix = new Matrix(Matrix.Identity(4));

            matrix[0, 0] = 1.0f - (yy + zz);
            matrix[0, 1] = xy - wz;
            matrix[0, 2] = xz + wy;

            matrix[1, 0] = xy + wz;
            matrix[1, 1] = 1.0f - (xx + zz);
            matrix[1, 2] = yz - wx;

            matrix[2, 0] = xz - wy;
            matrix[2, 1] = yz + wx;
            matrix[2, 2] = 1.0f - (xx + yy);

            return matrix;
        }

        private void MapToSphere(Point point, Size size, out Vector3D newVector)
        {
            //  Set adjustment factor for width and height.
            float adjustWidth = 1.0f / ((size.Width - 1.0f) * 0.5f);
            float adjustHeight = 1.0f / ((size.Height - 1.0f) * 0.5f);

            float scaledX = point.X * adjustWidth - 1.0f;
            float scaledY = 1.0f - point.Y * adjustHeight;

            //  Get square of length of point to point from centre.
            float length = scaledX * scaledX + scaledY * scaledY;

            //  Are we outside the sphere?
            if (length > 1.0f)
            {
                //  Get normalising factor.
                float norm = 1.0f / (float)Math.Sqrt(length);

                //  Return normalised point, a point on the sphere.
                newVector = new Vector3D(scaledX * norm, scaledY * norm, 0);
            }
            else
            {
                //  Return a point to a point mapped inside the sphere.
                newVector = new Vector3D(scaledX, scaledY, (float)Math.Sqrt(1.0f - length));
            }
        }

        #endregion Private Methods

        #region Protected Methods

        protected override void OnDeserializing()
        {
            base.OnDeserializing();

            transformationMatrix = defaultTransformationMatrix.Clone();
            lastTransformationMatrix = defaultLastTransformationMatrix.Clone();

            size = defaultSize;

            startVector = defaultStartVector;

            mouseStartDragPoint = defaultMouseStartDragPoint;
            isLeftDrag = defaultIsLeftDrag;
            isRightDrag = defaultIsRightDrag;
            isMiddleDrag = defaultIsMiddleDrag;
        }

        #endregion Protected Methods

        #region Public Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="ArcBallTransformation"/> class.
        /// </summary>
        public ArcBallTransformation()
        {
            transformationMatrix = defaultTransformationMatrix.Clone();
            lastTransformationMatrix = defaultLastTransformationMatrix.Clone();

            size = defaultSize;

            startVector = defaultStartVector;

            mouseStartDragPoint = defaultMouseStartDragPoint;
            isLeftDrag = defaultIsLeftDrag;
            isRightDrag = defaultIsRightDrag;
            isMiddleDrag = defaultIsMiddleDrag;
        }

        #endregion Public Constructors

        #region Public Methods

        /// <summary>
        /// Performs the transformation on the current matrix.
        /// </summary>
        public override void Transform(OpenGL gl)
        {
            if (OpenGL.IsValidContext(gl))
            {
                gl.MultMatrix(transformationMatrix.AsColumnMajorArray);
            }
        }

        /// <summary>
        /// Reset to default state.
        /// </summary>
        public override void Reset()
        {
            transformationMatrix.SetIdentity();

            NotifyPropertyChanged("TransformationMatrix");

            lastTransformationMatrix.SetIdentity();
        }

        public void HandleMouseDown(MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left || e.Button == MouseButtons.Middle || e.Button == MouseButtons.Right)
            {
                this.mouseStartDragPoint = e.Location;

                this.lastTransformationMatrix.FromOtherMatrix(this.transformationMatrix, this.transformationMatrix.Rows, this.transformationMatrix.Columns);

                switch (e.Button)
                {
                    case MouseButtons.Left:
                        this.isLeftDrag = true;
                        MapToSphere(this.mouseStartDragPoint, this.size, out this.startVector);
                        break;

                    case MouseButtons.Middle:
                        this.isMiddleDrag = true;
                        break;

                    case MouseButtons.Right:
                        this.isRightDrag = true;
                        break;
                }
            }
        }

        public void HandleMouseUp(MouseEventArgs e)
        {
            switch (e.Button)
            {
                case MouseButtons.Left:
                    this.isLeftDrag = false;
                    break;

                case MouseButtons.Middle:
                    this.isMiddleDrag = false;
                    break;

                case MouseButtons.Right:
                    this.isRightDrag = false;
                    break;
            }
        }

        public void HandleMouseMove(MouseEventArgs e)
        {
            Vector3D currentVector;

            MapToSphere(e.Location, this.size, out currentVector);

            if (isLeftDrag) // Rotate
            {
                Quaternion quaternion = CalculateQuaternion(startVector, currentVector);

                this.transformationMatrix = RotationMatrixFromQuaternion(quaternion) * this.lastTransformationMatrix;

                NotifyPropertyChanged("TransformationMatrix");
            }
            else if (isMiddleDrag) // Zoom
            {
                double len = Math.Sqrt(this.mouseStartDragPoint.X * this.mouseStartDragPoint.X + this.mouseStartDragPoint.Y * this.mouseStartDragPoint.Y)
                    / Math.Sqrt(e.X * e.X + e.Y * e.Y);

                this.transformationMatrix = TransformationHelper.ScaleMatrix(len, len, len) * this.lastTransformationMatrix;
                
                NotifyPropertyChanged("TransformationMatrix");
            }
            else if (isRightDrag) // Pan
            {
                Vector3D translation = new Vector3D((float)(e.X - mouseStartDragPoint.X) / (float)this.size.Width, (float)(mouseStartDragPoint.Y - e.Y) / (float)this.size.Height, 0.0f);

                this.transformationMatrix = TransformationHelper.TranslateMatrix(translation.X, translation.Y, translation.Z) * this.lastTransformationMatrix; //matrix;

                NotifyPropertyChanged("TransformationMatrix");
            }
        }

        public void HandleMouseWheel(MouseEventArgs e)
        {
            float scaleFactor = 1.0f + e.Delta / SystemInformation.MouseWheelScrollDelta / 10.0f;

            if (scaleFactor <= 0.0f)
                scaleFactor = Epsilon;

            this.lastTransformationMatrix.FromOtherMatrix(this.transformationMatrix, this.transformationMatrix.Rows, this.transformationMatrix.Columns);
            this.transformationMatrix = TransformationHelper.ScaleMatrix(scaleFactor, scaleFactor, scaleFactor) * this.lastTransformationMatrix;

            NotifyPropertyChanged("TransformationMatrix");
        }

        public void SetBounds(Size size)
        {
            this.size = size;
        }

        #endregion Public Methods

        #region Properties

        /// <summary>
        /// Gets the transformation matrix.
        /// </summary>
        /// <value>
        /// The transformation matrix.
        /// </value>
        public override Matrix TransformationMatrix
        {
            get { return transformationMatrix;  }
        }

        #endregion
    }
}
